import AnimationHelper from './AnimationHelper.js'

const INFLEXION = 0.35
const START_TENSION = 0.5
const END_TENSION = 1.0
const P1 = START_TENSION * INFLEXION
const P2 = 1.0 - END_TENSION * (1.0 - INFLEXION)

const NB_SAMPLES = 100
const GRAVITY_EARTH = 9.80665
const DECELERATION_RATE = Math.log(0.78) / Math.log(0.9)

/**
 * 惯性滚动动画
 */
class FlingAnimator {
    constructor(interval) {
        this.SPLINE_POSITION = []
        this.SPLINE_TIME = []

        this.ppi = 160

        this.startX = 0
        this.startY = 0
        this.finalX = 0
        this.finalY = 0

        this.minX = 0
        this.maxX = 0
        this.minY = 0
        this.maxY = 0

        this.currX = 0
        this.currY = 0
        this.deltaX = 0
        this.deltaY = 0

        this.velocity = 0
        this.currVelocity = 0
        this.distance = 0

        this.flingFriction = 0.015
        this.physicalCoeff = this.computeDeceleration(0.84)

        this.helper = new AnimationHelper(interval)

        this.init()
    }

    init() {
        let x_min = 0.0
        let y_min = 0.0
        for (let i = 0; i < NB_SAMPLES; i++) {
            const alpha = i / NB_SAMPLES

            let x_max = 1.0
            let x, tx, coef
            while (true) {
                x = x_min + (x_max - x_min) / 2.0
                coef = 3.0 * x * (1.0 - x)
                tx = coef * ((1.0 - x) * P1 + x * P2) + x * x * x
                if (Math.abs(tx - alpha) < 1E-5) break
                if (tx > alpha) x_max = x
                else x_min = x
            }
            this.SPLINE_POSITION[i] = coef * ((1.0 - x) * START_TENSION + x) + x * x * x

            let y_max = 1.0
            let y, dy
            while (true) {
                y = y_min + (y_max - y_min) / 2.0
                coef = 3.0 * y * (1.0 - y)
                dy = coef * ((1.0 - y) * START_TENSION + y) + y * y * y
                if (Math.abs(dy - alpha) < 1E-5) break
                if (dy > alpha) y_max = y
                else y_min = y
            }
            this.SPLINE_TIME[i] = coef * ((1.0 - y) * P1 + y * P2) + y * y * y
        }
        this.SPLINE_POSITION[NB_SAMPLES] = this.SPLINE_TIME[NB_SAMPLES] = 1.0
    }

    /**
     * 动画是否结束
     */
    isFinished() {
        return this.finished
    }

    /**
     * 结束动画
     */
    forceFinished(finished) {
        this.finished = finished
    }

    /**
     * 获取计算后的x坐标
     */
    getCurrX() {
        return this.currX
    }

    /**
     * 获取计算后的y坐标
     */
    getCurrY() {
        return this.currY
    }

    /**
     * 计算下一帧动画的x与y的坐标，并判断动画是否结束
     */
    compute() {
        return this.helper.compute(t => {
            const index = parseInt(NB_SAMPLES * t)
            let distanceCoef = 1
            let velocityCoef = 0
            if (index < NB_SAMPLES) {
                const t_inf = index / NB_SAMPLES
                const t_sup = (index + 1) / NB_SAMPLES
                const d_inf = this.SPLINE_POSITION[index]
                const d_sup = this.SPLINE_POSITION[index + 1]
                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf)
                distanceCoef = d_inf + (t - t_inf) * velocityCoef
            }

            this.currVelocity = velocityCoef * this.distance / this.duration * 1000.0

            this.currX = this.startX + Math.round(distanceCoef * (this.finalX - this.startX))
            this.currX = Math.min(this.currX, this.maxX)
            this.currX = Math.max(this.currX, this.minX)

            this.currY = this.startY + Math.round(distanceCoef * (this.finalY - this.startY))
            this.currY = Math.min(this.currY, this.maxY)
            this.currY = Math.max(this.currY, this.minY)

            if (this.currX == this.finalX && this.currY == this.finalY) {
                this.helper.finish()
            }
        }, () => {
            this.currX = this.finalX
            this.currY = this.finalY
        })
    }

    /**
     * 开启惯性滚动动画
     * @param startX 动画开始时的x坐标
     * @param startY 动画开始时的y坐标
     * @param velocityX x轴的初始速度
     * @param velocityY y轴的初始速度
     * @param minX x轴最小滚动距离
     * @param minY y轴最小滚动距离
     * @param maxX x轴最大滚动距离
     * @param maxY y轴最大滚动距离
     */
    fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) {
        let velocity = Math.hypot(velocityX, velocityY)
        this.velocity = velocity

        this.helper.start(this.getSplineFlingDuration(velocity))

        this.startX = startX
        this.startY = startY

        let coeffX = velocity == 0 ? 1.0 : velocityX / velocity
        let coeffY = velocity == 0 ? 1.0 : velocityY / velocity

        let totalDistance = this.getSplineFlingDistance(velocity)
        this.distance = totalDistance * this.signum(velocity)

        this.minX = minX
        this.maxX = maxX
        this.minY = minY
        this.maxY = maxY

        this.finalX = startX + Math.round(totalDistance * coeffX)
        this.finalX = Math.min(this.finalX, this.maxX)
        this.finalX = Math.max(this.finalX, this.minX)

        this.finalY = startY + Math.round(totalDistance * coeffY)
        this.finalY = Math.min(this.finalY, this.maxY)
        this.finalY = Math.max(this.finalY, this.minY)
    }

    /**
     * 运行惯性滚动动画
     * @param startX 动画开始时的x坐标
     * @param startY 动画开始时的y坐标
     * @param velocityX x轴的初始速度
     * @param velocityY y轴的初始速度
     * @param minX x轴最小滚动距离
     * @param minY y轴最小滚动距离
     * @param maxX x轴最大滚动距离
     * @param maxY y轴最大滚动距离
     * @param task 动画每一帧的回调
     */
    runFling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, task) {
        this.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY)

        this.helper.run(() => {
            return this.compute()
        }, () => {
            if (task) task(this.getCurrX(), this.getCurrY())
        })
    }

    getSplineDeceleration(velocity) {
        return Math.log(INFLEXION * Math.abs(velocity) / (this.flingFriction * this.physicalCoeff))
    }

    getSplineFlingDuration(velocity) {
        const l = this.getSplineDeceleration(velocity)
        const decelMinusOne = DECELERATION_RATE - 1.0
        return 1000.0 * Math.exp(l / decelMinusOne)
    }

    getSplineFlingDistance(velocity) {
        const l = this.getSplineDeceleration(velocity)
        const decelMinusOne = DECELERATION_RATE - 1.0
        return this.flingFriction * this.physicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l)
    }

    computeDeceleration(friction) {
        return GRAVITY_EARTH
        * 39.37
        * this.ppi
        * friction
    }

    signum(x) {
        return x > 0 ? 1 : x < 0 ? -1 : 0;
    }
}

export default FlingAnimator